/**
 * Copyright 2010, Lars J. Nilsson <http://www.larsan.net> 
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.larsan.dconf;

import static net.larsan.dconf.util.ExtendedValidate.notNull;
import static org.apache.commons.lang.Validate.notNull;

import java.text.ParseException;
import java.util.LinkedList;

/**
 * <p>The path denotes the location of a specific node within the 
 * configuration hierarchy. It includes information about domain,
 * path and it's own name.</p>
 * 
 * <p>A path is immutable.</p>
 * 
 * <p>A path is composed of three parts, domain, path and a name. 
 * All three my contain any character except ':' and '/'. The ':'
 * separates the domain from the path, and the '/' separates path
 * component.</p>
 * 
 * <p>Below follows a few examples of legal paths: </p>
 * 
 * <pre>
 *   dev:/service/auth/url
 *   tech:/com/company/Rooster
 *   tcp:/multicast
 *   root:/
 * </pre>
 * 
 * <p>The root of each path hiearachy is a path which has a null parent
 * and an empty name, this is the equivalent of a full path looking like
 * this (for domain 'tech'):
 * 
 * <pre>
 *    tech:/
 * </pre>
 * 
 * <p>The above path is an exception, all paths except a root path must
 * contain an name. </p>
 * 
 * @author Lars J. Nilsson
 */
public final class DPath {
	
	/**
	 * Parse a path object from a string. The path must be absolute, and
	 * legal, containing both domain and full path.
	 * 
	 * @param path Path to parse, must be absolute, must not be null
	 * @return A path object, never null
	 * @throws ParseException On illegal paths
	 */
	public static final DPath parse(String path) throws ParseException {
		notNull(path);
		int i = path.indexOf(":/");
		if(i == -1) throw new ParseException("Missing domain in path '" + path + "'", 0);
		String dom = path.substring(0, i);
		String rootPath = path.substring(i + 1);
		return parseAbsolute(dom, rootPath);
	}

	
	/**
	 * Parse a path object from a string, relative to the given parent. The
	 * path must not be absolute, and the new path will be a descendant of 
	 * the parent.
	 * 
	 * @param parent Parent path to use, must not be null
	 * @param path Path to parse, must not be null
	 * @return A new path, descendant of the paremt, never null
	 * @throws ParseException On illegal paths
	 */
	public static final DPath parseFrom(DPath parent, String path) throws ParseException {
		notNull(path);
		int i = path.indexOf(":/");
		if(i != -1) {
			return parse(path);
		} else {
			notNull(parent);
			if(path.startsWith("/")) {
				return parseAbsolute(parent.getDomain(), path);
			} else {
				String fp = parent.getFullPath();
				return parse(fp + "/" + path);
			}
		}
	}
	
	// --- INSTANCE MEMBERS --- //
	
	private final String name;
	private final String path;
	private final String domain;
	
	private final int hashCode;
	private final String fullPath;
	private final String localPath;
	
	/**
	 * @param domain Domain of the node, must not be null
	 * @param path Path of the node, excluding name, must not be null
	 * @param name Name of the node, must not be null
	 */
	DPath(String domain, String path, String name) {
		notNull(domain, path, name);
		this.path = path;
		this.domain = domain;
		this.name = name;
		/*
		 * Warning: premature optimization below... :-)
		 */
		this.localPath = path + name;
		this.fullPath = domain + ":" + localPath;
		this.hashCode = fullPath.hashCode();
	}
	
	
	/**
	 * Get path, relative to the current path. This is a shortcut
	 * for {@link DNodeStore#parseFrom(DPath, String) parseFrom} method
	 * in the node store. A path can be both absolute and relative. Please
	 * refer to {@link DNodeStore} for more details. 
	 * 
	 * @param path Path to navigate to, must not be null
	 * @return A new path object, never null
	 * @throws ParseException On illegal paths
	 */
	public DPath navigate(String path) throws ParseException {
		notNull(path);
		return parseFrom(this, path);
	}
	
	
	/**
	 * Get a child to the current node, by name. The given name must not
	 * contain the characters ':' or '/'.
	 * 
	 * @param name Name of the child to get, must not be null nor contain '/' or ':'
	 * @return A child path object, or null if not found
	 */
	public DPath child(String name) {
		notNull(name);
		if(name.indexOf(':') != -1) throw new IllegalArgumentException("Child name must not contain ':', was: " + name);
		if(name.indexOf('/') != -1) throw new IllegalArgumentException("Child name must not contain '/', was: " + name);
		try {
			return parseFrom(this, name);
		} catch (ParseException e) {
			throw new IllegalStateException(e);
		}
	}
	
	
	/**
	 * @return The domain name, never null
	 */
	public String getDomain() {
		return domain;
	}
	
	
	/**
	 * @return The node name, never null
	 */
	public String getName() {
		return name;
	}
	
	
	/**
	 * @return The parent path, or null if root
	 */
	public DPath getParent() {
		if(path.equals("/") && name.length() == 0) {
			return null;
		} else {
			try {
				return parse(domain + ":" + (path.equals("/") ? path : path.substring(0, path.length() - 1)));
			} catch (ParseException e) {
				throw new IllegalStateException(e);
			}
		}
	}
	
	
	/**
	 * @return The path, including name but excluding domain, of the node, never null
	 */
	public String getPath() {
		return localPath;
	}
	
	
	/**
	 * @return The path, including both name and domain, never null
	 */
	public String getFullPath() {
		return fullPath;
	}
	
	
	// --- COMMON OBJECT METHODS --- //
	
	@Override
	public String toString() {
		return this.fullPath;
	}
	
	@Override
	public boolean equals(Object o) {
		if(!(o instanceof DPath)) return false;
		return ((DPath)o).hashCode == this.hashCode;
	}
	
	@Override
	public int hashCode() {
		return hashCode;
	}
	
	
	// --- PRIVATE STATIC METHODS --- //
	
	// --- PRIVATE METHODS --- //
	
	private static DPath parseAbsolute(String dom, String rootPath) throws ParseException {
		rootPath = normalize(rootPath);
		int j = rootPath.lastIndexOf('/');
		String name = rootPath.substring(j + 1);
		String nodePath = rootPath.substring(0, j + 1);
		return new DPath(dom, nodePath, name);
	}
	
	private static String normalize(String path) throws ParseException {
		LinkedList<String> list = normalizeToList(path);
		return listToString(list);
	}

	private static String listToString(LinkedList<String> list) {
		StringBuilder b = new StringBuilder();
		if(list.size() > 0) {
			for (String s : list) {
				b.append("/").append(s);
			}
		} else {
			b.append("/");
		}
		return b.toString();
	}

	private static LinkedList<String> normalizeToList(String path) throws ParseException {
		String[] comps = path.split("/");
		LinkedList<String> list = new LinkedList<String>();
		for (String s : comps) {
			if(s.equals(".") || s.length() == 0) {
				// ignore
			} else if(s.equals("..")) {
				if(list.size() == 0) throw new ParseException("Illegal parent denominator in path: " + path, 0);
				list.removeLast();
			} else {
				list.add(s);
			}
		}
		return list;
	}
}
